package hydrap2p.proxy.manager;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.LinkedList;

public class InfiniteCircularBuffer {
	/**
	 * A chunk of data in the buffer 
	 * @author Hufman
	 *
	 */
	private class Chunk
	{
		private static final int CHUNK_SIZE=2048;
		private byte[] data;
		private int offset;
		private int markoffset;
		private int length;
		
		public Chunk()
		{
			data=new byte[CHUNK_SIZE];
			offset=0;
			markoffset=0;
			length=0;
		}
		
		public void clear()
		{
			offset=0;
			markoffset=0;
			length=0;
		}
		
		/** InputStream emulation */
		public int available()
		{
			return length-offset;
		}
		public int read()
		{
			if (offset<length)
				return data[offset++];
			return -1;
		}
		public int read(byte[] b)
		{
			return read(b,0,b.length);
		}
		public int read(byte[] b, int off, int len)
		{
			if (offset==length)
				return -1;
			int destlen=Math.min(b.length-off, len);
			if (destlen<=0)
				return 0;
			int reallen=Math.min(length-offset, destlen);
			
			if (len>0)
				System.arraycopy(data, offset, b, off, reallen);
			offset+=reallen;
			return reallen;
		}
		public void mark(int readlimit)
		{
			markoffset=offset;
		}
		public void reset()
		{
			offset=markoffset;
		}
		public int skip(int len)
		{
			int reallen=Math.min(length-offset, len);
			offset+=reallen;
			return reallen;
		}
		
		/** OutputStream emulation */
		public int free()
		{
			return data.length-length;
		}
		public void write(int b)
		{
			if (length==data.length)
				throw new IndexOutOfBoundsException("Full buffer");
			data[length++]=(byte) b;
		}
		public void write(byte[] b)
		{
			write(b, 0, b.length);
		}
		public void write(byte[] b, int off, int len)
		{
			if (b==null)
				throw new NullPointerException("B is null");
			if (off<0 || len<0)
				throw new IndexOutOfBoundsException("Negative parameters");
			if (off+len>b.length)
				throw new IndexOutOfBoundsException("Attempt to write too much from source buffer");
			if (data.length-length<len)
				throw new IndexOutOfBoundsException("Attempt to write too much to data buffer");

			System.arraycopy(b, off, data, length, len);
			length+=len;
		}
	}

	private class InfiniteInputStream extends InputStream
	{
		private boolean blocking;
		public InfiniteInputStream(boolean blocking)
		{
			this.blocking=blocking;
		}
		@Override
		public int read() throws IOException {
			synchronized(read)
			{
				if (readchunk.available()==0 && readchunk.free()==0)
				{
					if (markedchunk!=null)
					{
						read.add(readchunk);
					}
					else
					{
						readchunk.clear();
						empty.add(readchunk);
					}
					
					if (!unread.isEmpty())
					{
						readchunk=unread.remove(0);
					}
					else
						readchunk=writechunk;
					
				}
				int ret=readchunk.read();
				if (ret<0 && blocking)
				{
					while (ret<0)
					{
						try {
							read.wait();
						} catch (InterruptedException e) {
							ret=readchunk.read();
						}
					}
				}
				if (ret>=0)
				{
					available--;
					if (markedchunk!=null)
					{
						marklimit--;
						markedread++;
						if (marklimit<0)		// ran out of mark space, clear it out
						{
							markedchunk=null;
							clearReadList();
						}
					}
				}
				return ret;
			}
		}
		
		@Override
		public int read(byte[] buf)
		{
			return read(buf, 0, buf.length);
		}
		
		@Override
		public int read(byte[] buf, int off, int len)
		{
			synchronized(read)
			{
				int totalread=0;
				int thisread=0;
				while (len>0 && thisread>=0)
				{
					// no more to read from this chunk, load the next chunk
					if (readchunk.available()==0 && readchunk.free()==0)
					{
						if (markedchunk!=null)
						{
							read.add(readchunk);
						}
						else
						{
							readchunk.clear();
							empty.add(readchunk);
						}
						
						if (!unread.isEmpty())
						{
							readchunk=unread.remove(0);
						}
						else
							readchunk=writechunk;
						
					}
					thisread=readchunk.read(buf, off, len);
					if (thisread<0 && blocking)
					{
						while (thisread<0)
						{
							try {
								read.wait();
							} catch (InterruptedException e) {
								thisread=readchunk.read(buf, off, len);
							}
						}
					}
					if (thisread>=0)
					{
						totalread+=thisread;
						available-=thisread;
						off+=thisread;
						len-=thisread;
						if (markedchunk!=null)
						{
							marklimit-=thisread;
							markedread+=thisread;
							if (marklimit<0)		// ran out of mark space, clear it out
							{
								markedchunk=null;
								clearReadList();
							}
						}
					}
				}
				return totalread;
			}
		}
		
		@Override
		public int available()
		{
			synchronized(read)
			{
				return available;
			}
		}
		
		@Override
		public boolean markSupported()
		{
			return true;
		}
		
		@Override
		public void mark(int readlimit)
		{
			synchronized(read)
			{
				if (readlimit<=0)
					return;
	
				clearReadList();			// clear out any old marks
				markedchunk=readchunk;
				markedchunk.mark(0);
				marklimit=readlimit;
				markedread=0;
			}
		}
		
		@Override
		public void reset() throws IOException
		{
			synchronized(read)
			{
				if (markedchunk==null)
				{
					throw new IOException("No valid mark");
				}
				if (markedchunk!=readchunk)
				{
					readchunk.reset();
					if (readchunk!=writechunk)
						unread.add(0, readchunk);
					if (read.size()>0)				// remove the marked chunk from the beginning of the read list
						read.remove(0);
					for (Chunk chunk:read)
					{
						chunk.reset();
					}
					unread.addAll(0, read);
					readchunk=markedchunk;
				}
				readchunk.reset();
				marklimit+=markedread;
				available+=markedread;
			}
		}
	}
	
	private class InfiniteOutputStream extends OutputStream
	{
		@Override
		public void write(int arg0) throws IOException
		{
			synchronized(read)
			{
				if (writechunk.free()==0)
				{
					if (writechunk!=readchunk)
					{
						unread.add(writechunk);
					}
					if (!empty.isEmpty())
						writechunk=empty.remove(0);
					else
						writechunk=new Chunk();
				}
				writechunk.write(arg0);	
				available++;
				read.notifyAll();
			}
			tryPrune();
		}
		
		@Override
		public void write(byte[] buf, int off, int len)
		{
			synchronized(read)
			{
				int thiswrite;
				while (len>0)
				{
					// no more to read from this chunk, load the next chunk
					if (writechunk.free()==0)
					{
						if (writechunk!=readchunk)
						{
							unread.add(writechunk);
						}
						if (!empty.isEmpty())
							writechunk=empty.remove(0);
						else
							writechunk=new Chunk();
					}
					thiswrite=Math.min(writechunk.free(), len);
					writechunk.write(buf, off, thiswrite);
					available+=thiswrite;
					off+=thiswrite;
					len-=thiswrite;
				}
			}
		}
		
		public int writeFromInputStream(InputStream input, int len) throws IOException
		{
			synchronized(read)
			{
				int thiswrite;
				int totalwrite=0;
				while (len>0 && input.available()>0)
				{
					// no more to read from this chunk, load the next chunk
					if (writechunk.free()==0)
					{
						if (writechunk!=readchunk)
						{
							unread.add(writechunk);
						}
						if (!empty.isEmpty())
							writechunk=empty.remove(0);
						else
							writechunk=new Chunk();
					}
					thiswrite=Math.min(writechunk.free(), len);
					thiswrite=input.read(writechunk.data, writechunk.length, thiswrite);
					writechunk.length+=thiswrite;
					available+=thiswrite;
					len-=thiswrite;
					totalwrite+=thiswrite;
				}
				return totalwrite;
			}
		}
	}
	
	private LinkedList<Chunk> empty;		// chunks that have been previously allocated and discarded
	private Chunk markedchunk;				// the first chunk that we have to save, because of being marked
	private LinkedList<Chunk> read;			// chunks that have been read, but we have to save them for marking purposes
	private Chunk readchunk;					// the current chunk that is being read from
	private LinkedList<Chunk> unread;		// chunks that have been fully written to but not read yet
	private Chunk writechunk;				// the current chunk that is being written to
	
	private int available;					// how much data is available for reading
	private int marklimit;					// how much data can be read before the mark becomes invalid
	private int markedread;					// how much data has been read since we were marked, to be added to the available when we reset
	
	private long lastprune;
	private static int PRUNE_TIME=2000;		// ms before pruning free chunks  
	
	private InfiniteInputStream inputstream;
	private InfiniteInputStream blockinginputstream;
	private InfiniteOutputStream outputstream;
	
	public InfiniteCircularBuffer()
	{
		empty=new LinkedList<Chunk>();
		markedchunk=null;
		read=new LinkedList<Chunk>();
		readchunk=new Chunk();
		unread=new LinkedList<Chunk>();
		writechunk=readchunk;
		available=0;
		marklimit=0;
		markedread=0;
		lastprune=0;
		inputstream=new InfiniteInputStream(false);
		blockinginputstream=new InfiniteInputStream(true);
		outputstream=new InfiniteOutputStream();
	}
	public InputStream getInputStream(boolean blocking)
	{
		if (blocking)
			return blockinginputstream;
		else
			return inputstream;
	}
	public OutputStream getOutputStream()
	{
		return outputstream;
	}
	public int writeFromInputStream(InputStream input, int len) throws IOException
	{
		return outputstream.writeFromInputStream(input, len);
	}
	private void clearReadList()
	{
		while (!read.isEmpty())
		{
			Chunk temp=read.remove(0);
			temp.clear();
			empty.add(temp);
		}
	}
	/**
	 * Prune a free Chunk
	 */
	private void tryPrune()
	{
		synchronized(read)
		{
			if (System.currentTimeMillis()>lastprune+PRUNE_TIME && !empty.isEmpty())
			{
				empty.remove(0);
			}
		}
	}
}
